自定义View(8) -- 汽车之家折叠列表

先看看汽车之家折叠列表的效果图
汽车之家折叠列表

接着看看实现的效果图

实现的效果

在这篇文章中主要采用ViewDragHelper这个类,这个是系统提供的一个处理view拖动的一个类。具体请查看相关资料,在这就不多说。
先来解析实现的思路,view的移动采用ViewDragHelper即可,如果下方是一般的View的话就差不多了,但是如果是ListView或者RecyclerView之类的话主要处理一个事件拦截的逻辑。首先要清楚ListView或者RecyclerView在处理事件的时候调用了getParent().requestDisallowInterceptTouchEvent(true);请求父布局不拦截事件,所以当拦截的时候不能让ListView或者RecyclerView接受到MOVE事件。逻辑很简单,就是当下面的ListView或者RecyclerView到顶部 并且是下拉的时候就需要使用ViewDragHelper来响应拖动,如果上面的菜单是打开状态的话那么也需要响应,这时候就需要拦截MOVE事件来处理拖动。逻辑就是这么简单,但是细节的东西有很多,不能马虎并且熟悉相关的api


接下来开始撸码
这里我选择继承FrameLayout,在初始化的时候创建ViewDragHelper,资源加载完毕了得到需要拖动的mDragView,在测量之后获取到最大拖动的距离,也就是上方菜单的高度,当手指抬起的时候判断是需要关闭还是打开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class VerticalDragListView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0)
: FrameLayout(context, attrs, defStyleAttr) {

private var mDragView: View? = null//拖动的view
private var mMenuViewHeight: Int = 0 //拖动的view 高度
private var mMenuIsOpen: Boolean = false//是否打开
private var mViewDragHelper: ViewDragHelper? = null //拖动的辅助类

private val mCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
//指定view是否可以拖动
override fun tryCaptureView(child: View, pointerId: Int): Boolean {
return mDragView == child
}

//返回移动的距离
override fun clampViewPositionVertical(child: View?, top: Int, dy: Int): Int {
//滑动的范围只能是在menu的高度
var t: Int = top
if (top <= 0) t = 0
if (top >= mMenuViewHeight) t = mMenuViewHeight
return t
}

//手松开的时候回调 打开还是关闭
override fun onViewReleased(releasedChild: View?, xvel: Float, yvel: Float) {
//打开菜单
if (mDragView!!.top >= mMenuViewHeight / 2) {
mViewDragHelper?.settleCapturedViewAt(0, mMenuViewHeight)
mMenuIsOpen = true
} else {//关闭菜单
mViewDragHelper?.settleCapturedViewAt(0, 0)
mMenuIsOpen = false
}
invalidate()
}
}

//响应滚动
override fun computeScroll() {
if (mViewDragHelper!!.continueSettling(true)) invalidate()
}


init {
mViewDragHelper = ViewDragHelper.create(this, mCallback)
}

override fun onFinishInflate() {
super.onFinishInflate()
if (childCount != 2) throw RuntimeException("childCount只能包含两个子布局")
mDragView = getChildAt(1)
}

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (changed) mMenuViewHeight = getChildAt(0).measuredHeight
}


override fun onTouchEvent(event: MotionEvent?): Boolean {
mViewDragHelper?.processTouchEvent(event)
return true
}

}

在这需要注意一点,当手指松开判断打开或者关闭菜单需要调用invalidate()并且重写computeScroll()函数来响应。

如果下方的view不是ListView或者RecyclerView之类的话,到这就可以了,但是实际开发中,下方一般是这种,所以就需要按照上面说的处理事件拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
   private var mDownY: Float = 0.0f
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
// 菜单打开要拦截
if (mMenuIsOpen) {
return true
}

// 向下滑动拦截,不让ListView或者RecyclerView做处理
// 谁拦截谁 父View拦截子View ,但是子 View 可以调这个方法
// requestDisallowInterceptTouchEvent 请求父View不要拦截,改变的其实就是 mGroupFlags 的值
when (ev!!.action) {
MotionEvent.ACTION_DOWN -> {
mDownY = ev.y
// 让 DragHelper 拿一个完整的事件
mViewDragHelper!!.processTouchEvent(ev)
}

MotionEvent.ACTION_MOVE -> {
val moveY = ev.y
if (moveY - mDownY > 0 && !canChildScrollUp()) {
// 向下滑动 && 滚动到了顶部,拦截不让ListView或者RecyclerView做处理
return true
}
}
}
return super.onInterceptTouchEvent(ev)
}

/**
* @return Whether it is possible for the child view of this layout to
* * scroll up. Override this if the child view is a custom view.
*/
fun canChildScrollUp(): Boolean {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mDragView is AbsListView) {
val absListView = mDragView as AbsListView
return absListView.childCount > 0 && (absListView.firstVisiblePosition > 0 || absListView.getChildAt(0)
.top < absListView.paddingTop)
} else {
return ViewCompat.canScrollVertically(mDragView, -1) || mDragView!!.scrollY > 0
}
} else {
return ViewCompat.canScrollVertically(mDragView, -1)
}
}

这里需要注意,如果不在ACTION_DOWN的时候调用mViewDragHelper.processTouchEvent(ev)的话,那么ViewDragHelper将会报错,将不会触发拖动事件

从字面意思都可以看出需要一个完整的事件,所以需要在ACTION_DOWN的时候调用ViewDragHelper.processTouchEvent(ev)


在一步步的分析之下,这个效果就慢慢的完成了。有了新需求的时候,在动手应该理清思路,然后想好使用相关的api,处理一些手势可以使用OnGestureListener,处理拖动可以使用ViewDragHelper,这些都是系统封装好的辅助类,应该要合理的利用这些辅助类。相信如果不使用这些辅助类也可以写出这些效果,但是那样的话也会浪费大量的事件和精力,而且很容易出错。

本文源码下载地址:https://github.com/ChinaZeng/CustomView

-------------The End-------------